/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/**
 * Code for drawing the connected circles that make up the logical plan for this topology.
 * Exports a single global function drawLogicalPlan.
 */
(function (global) {
  function htmlDecode(input){
    var e = document.createElement('div');
    e.innerHTML = input;
    return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  }

  var sleep = function(delay) {
    var start = new Date().getTime();
    while (new Date().getTime() < start + delay);
  };

  // Rearrage nodes in the groups so that
  // the number of intersections of edges
  // is minimized.
  // Ideally, there should be no intersections,
  // but if that is not achievable, rearrage
  // so as to have minimum intersections.
  var minimizeIntersections = function (groups) {
    // First assign group level index.
    // Also set the source group level indices.
    for (var i = 0; i < groups.length; i++) {
      for (var j = 0; j < groups[i].length; j++) {
        groups[i][j].groupsLevelIndex = j;
        for (var e = 0; e < groups[i][j].edges.length; e++) {
          var node = groups[i][j].edges[e].target;
          if (node.sourceGroupsLevelIndices === undefined) {
            node.sourceGroupsLevelIndices = [];
          }
          node.sourceGroupsLevelIndices.push(j);
        }
      }
    }

    var compare = function (a, b) {
      var aIndices = a.sourceGroupsLevelIndices;
      var bIndices = b.sourceGroupsLevelIndices;
      var sumA = 0;
      var sumB = 0;
      for (var i = 0; i < aIndices.length; i++) {
        sumA += aIndices[i];
      }
      for (var i = 0; i < bIndices.length; i++) {
        sumB += bIndices[i];
      }
      var avgA = sumA / aIndices.length;
      var avgB = sumB / bIndices.length;

      return avgA - avgB;
    }

    // Sort individual groups based on avg sourceGroupsLevelIndices.
    // Don't forget to update its own groupLevelIndex and
    // next level nodes' sourceGroupsLevelIndices.
    // Need to start from 1, since 0 level nodes won't have
    // any sourceGroupsLevelIndices.
    for (var i = 1; i < groups.length; i++) {
      groups[i].sort(compare);
      for (var j = 0; j < groups[i].length; j++) {
        groups[i][j].groupsLevelIndex = j;
        // Reset the next level's sourceGroupsLevelIndices.
        for (var e = 0; e < groups[i][j].edges.length; e++) {
          groups[i][j].edges[e].target.sourceGroupsLevelIndices = undefined;
        }
      }
      for (var j = 0; j < groups[i].length; j++) {
        groups[i][j].groupsLevelIndex = j;
        // Now set those values again.
        for (var e = 0; e < groups[i][j].edges.length; e++) {
          var node = groups[i][j].edges[e].target;
          if (node.sourceGroupsLevelIndices === undefined) {
            node.sourceGroupsLevelIndices = [];
          }
          node.sourceGroupsLevelIndices.push(j);
        }
      }
    }
    return groups;
  };

  // Sort the graph topologically
  // and also add virual nodes.
  var groupify = _.memoize(function (nodes, links) {

    nodes.forEach(function (node) {
        node.isReal = true;
    });

    var numNodes = nodes.length;

    var nodesToLinks = {};
    links.forEach(function (l) {
      if (!(l.source.name in nodesToLinks)) {
        nodesToLinks[l.source.name] = [];
      }
      nodesToLinks[l.source.name].push(l);
    });

    nodes.forEach(function (node) {
      var nextLinks = nodesToLinks[node.name];
      if (nextLinks) {
        node.edges = nodesToLinks[node.name];
      } else {
        node.edges = [];
      }
    });

    // Find the first elements
    var group = nodes.filter(function (node) {
      return links.every(function (link) {
        return node !== link.target;
      });
    });

    var groupIndex = 0;
    group.forEach(function (node) {
      node.groupIndex = groupIndex;
    });

    groupIndex++;
    var toProcess = [];
    var nextLinks = group.map(function (node) {
      return (nodesToLinks[node.name] === undefined) ? [] : nodesToLinks[node.name];
    });

    nextLinks.forEach(function (arr) {
      toProcess = toProcess.concat(arr);
    });

    while (toProcess.length > 0) {
      // In case of circular graphs, this will prevent infinte loop
      if (groupIndex > numNodes) {
        break;
      }

      group = toProcess.map(function (link) {
        return link.target;
      });

      group.forEach(function (node) {
        node.groupIndex = groupIndex;
      });

      nextLinks = group.map(function (node) {
        if (node.name in nodesToLinks) {
          return nodesToLinks[node.name];
        } else {
          return [];
        }
      });

      toProcess = [];
      if (nextLinks.length > 0) {
        nextLinks.forEach(function (arr) {
          toProcess = toProcess.concat(arr);
        });
      }
      groupIndex++;
    }

    var groups = Array.apply(null, new Array(groupIndex)).map(function (x) { return []; });

    nodes.forEach(function (node) {
      groups[node.groupIndex].push(node);
    });

    console.warn("done groupifying");
    console.warn("adding virtual nodes");

    for (var i = 0; i < groups.length; i++) {
      group = groups[i];
      group.forEach(function (node) {

        var edges = node.edges;
        edges.forEach(function (edge) {

          var diff = edge.target.groupIndex - node.groupIndex;
          if (diff > 1) {

            var newNode = {
              "groupIndex": i + 1,
              "edges": [],
              "isReal": false
            };

            newNode.edges.push({
              "source": newNode,
              "target": edge.target
            });

            edge.target = newNode;
            groups[i+1].push(newNode);
          }
        });
      });
    }

    console.warn("done adding virtual nodes");
    var goodGroups = minimizeIntersections(groups);
    return goodGroups;
  });

  // TODO: Too long function. Will Refactor.
  function drawLogicalPlan(planController, topology, id, outerWidth, outerHeight,
          cluster, environ, toponame) {

    id = id === undefined ? "#content" : id;
    outerWidth = outerWidth === undefined ? 1000 : outerWidth;
    outerHeight = outerHeight === undefined ? 300 : outerHeight;

    var padding = {
      top: 50,
      left: 30,
      right: 30,
      bottom: 50
    };

    var height = outerHeight - padding.top - padding.bottom;
    var width = outerWidth - padding.left - padding.right;

    // create the svg
    var outerSvg = d3.select(id).text("")
              .append("svg")
                .attr("width", outerWidth)
                .attr("height", outerHeight)
                .attr("id", "topology");
    var svg = outerSvg.append("g")
                .attr("tranform", "translate(" + padding.left + "," + padding.top + ")");

    spoutsArr = [];
    boltsArr = [];

    // create the spout array
    for (var i in topology.spouts) {
      spoutsArr.push({
        "name": i
      });
    }

    // create the bolt array
    for (var i in topology.bolts) {
      boltsArr.push({
          "name": i,
          "inputComponents": topology.bolts[i]["inputComponents"],
          "inputStreams": topology.bolts[i]["inputs"]
      });
    }

    var nodes = spoutsArr.concat(boltsArr)
    var links = [];

    for (var b in boltsArr) {
      for (w in nodes) {
        if (boltsArr[b].inputComponents.indexOf(nodes[w].name) >= 0) {
          // Found that node[w] is upstream of boltsArr[b], build a link
          var streams = []
          for (i in boltsArr[b].inputComponents) {
            if (boltsArr[b].inputComponents[i] == nodes[w].name) {
              streams.push(boltsArr[b].inputStreams[i].stream_name
                  + ":" + boltsArr[b].inputStreams[i].grouping);
            }
          }
          links.push({
            "source": nodes[w],
            "target": boltsArr[b],
            "streams": streams.sort().join("<br>")
          });
        }
      }
    }

    var sptblt = Object.keys(topology.spouts)
                       .concat(Object.keys(topology.bolts));

    var color = d3.scale.ordinal().domain(sptblt.sort()).range(colors);

    // Groupify
    var groups = groupify(nodes, links);

    // Done groupifying
    nodes = [];
    links = [];
    groups.forEach(function (group) {
      group.forEach(function (node) {
        nodes.push(node);
        node.edges.forEach(function (edge) {
          links.push(edge);
        });
      });
    });

    // layout using force directed graph
    var force = d3.layout.force()
                  .gravity(0.05)
                  .distance(100)
                  .charge(-1000)
                  .size([width, height]);

    force.nodes(nodes)
         .links(links)
         .linkDistance(200)
         .start();

    hOffset = 1.0 / (groups.length + 1);

    nodes.forEach(function (node) {
      var x = hOffset * (node.groupIndex + 1);
      var group = groups[node.groupIndex];
      var vOffset = 1.0 / (group.length + 1);
      var y = vOffset * (group.indexOf(node) + 1);
      // Interpolate it with lesser width
      // to account for nodes that are
      // in the extreme right. Otherwise, those
      // nodes would sometimes not respond to
      // hover function.
      node.x = d3.interpolate(0, width)(x);
      node.y = d3.interpolate(0, height)(y);
    });

    var yRange = d3.extent(nodes, function (d) { return d.y; });
    outerSvg.attr('height', (yRange[1] - yRange[0]) + padding.top + padding.bottom);
    svg.attr('transform', 'translate(' + padding.left + ',' + (padding.top - yRange[0]) + ')')

    var connection_tip = d3.tip()
        .attr('class', 'd3-tip main text-center connection')
        .offset([10, 0])
        .direction('s')
        .html(function (d) {
          return d.streams;
        });

    var node = svg.selectAll(".topnode")
                  .data(nodes)
                  .enter()
                  .append("g")
                  .attr("class", "topnode")
                  .style("fill", "black");

    // Links
    node.each(function (n) {
      d3.select(this)
        .selectAll(".link")
        .data(n.edges)
        .enter()
        .append("path")
        .attr('class', 'link')
        .attr("stroke-width", linestyle.boldwidth)
        .attr("stroke", linestyle.color)
        .attr("fill", "none")
        .attr("d", function (edge) {
          var p0 = edge.source;
          var p3 = edge.target;
          var m = (p0.x + p3.x) / 2;
          var p = [p0, {x: m, y: p0.y}, {x: m, y: p3.y}, p3];
          return "M" + p[0].x + " " + p[0].y +
                 "C" + p[1].x + " " + p[1].y +
                 " " + p[2].x + " " + p[2].y +
                 " " + p[3].x + " " + p[3].y;
        })
        .on('mouseover', connection_tip.show)
        .on('mouseout', connection_tip.hide);
    });

    // Component
    node.append("circle")
        .attr('class', 'background')
        .attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; })
        .attr("r", function (d) {
          if (d.isReal) {
            return d.r = 17;
          }
          return d.r = 0;
        })
        .style('fill', 'white');

    node.append("circle")
        .attr("class", "node")
        .attr("cx", function (d) { return d.cx = d.x; })
        .attr("cy", function (d) { return d.cy = d.y; })
        .attr("r", function (d) {
          if (d.isReal) {
            return d.r = 15;
          }
          return d.r = 0;
        })
        .style("stroke", linestyle.color)
        .style("stroke-width", linestyle.width)
        .style('fill', function (d) {
          d.defaultColor = color(d.name);
          d.color = d.color || d.defaultColor;
          return d.color;
        })
        .on("click", planController.logicalComponentClicked)
        .on("dblclick", planController.logicalComponentClicked)
        .on("mouseover", planController.logicalComponentHoverOver)
        .on("mouseout", planController.logicalComponentHoverOut);

    // Component name
    node.append("text")
        .attr("id", function(d) { return "text+" + d.name; })
        .attr("x", function (d) { return d.cx; })
        .attr("y", function (d) { return d.cy - d.r - 10; })
        .attr("class", "fade")
        .style("text-anchor", "middle")
        .style("user-select", "all")
        .text(function (d) {
          if (d.isReal) {
            return d.name;
          }
          return "";
        });


    svg.call(connection_tip);
  }

  global.drawLogicalPlan = drawLogicalPlan;
}(this));
